<?php

namespace common\components;

use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
use \yii\rbac\PhpManager;
use Yii;
use yii\rbac\Item;
use yii\rbac\Role;
use yii\rbac\Permission;
use yii\rbac\Assignment;
use yii\base\InvalidCallException;
use yii\base\InvalidParamException;
use yii\helpers\VarDumper;
use yii\rbac\Rule;

/**
 * PhpManager represents an authorization manager that stores authorization
 * information in terms of a PHP script file.
 *
 * The authorization data will be saved to and loaded from three files
 * specified by [[itemFile]], [[assignmentFile]] and [[ruleFile]].
 *
 * PhpManager is mainly suitable for authorization data that is not too big
 * (for example, the authorization data for a personal blog system).
 * Use [[DbManager]] for more complex authorization data.
 *
 * Note that PhpManager is not compatible with facebooks [HHVM](http://hhvm.com/) because
 * it relies on writing php files and including them afterwards which is not supported by HHVM.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Alexander Kochetov <creocoder@gmail.com>
 * @author Christophe Boulain <christophe.boulain@gmail.com>
 * @author Alexander Makarov <sam@rmcreative.ru>
 * @since  2.0
 */
class RbacPhpManager extends PhpManager
{

    public $originItems;
    public $originItemFile = null;

    public function setGenerateMode($init = true)
    {
        if ( ! $this->originItemFile)
        {
            $this->originItemFile = $this->itemFile;
        }
        $this->itemFile = $this->originItemFile . '.gen.php';
        if ($init)
        {
            $this->init();
        }
    }

    public function setCustomEditMode($init = true)
    {
        if ( ! $this->originItemFile)
        {
            $this->originItemFile = $this->itemFile;
        }
        $this->itemFile = $this->originItemFile . '.custom.php';
        if ($init)
        {
            $this->init();
        }
        if (empty($this->items))
        {
            $this->saveItems();
        }
    }

    public function setNormalMode($init = true)
    {
        $this->itemFile = $this->originItemFile;
        if ($init)
        {
            $this->init();
        }
    }

    public function loadGenerateDataFromFile()
    {
        if ( ! $this->originItemFile)
        {
            $this->originItemFile = $this->itemFile;
        }
        $path = Yii::getAlias($this->originItemFile . '.gen.php');
        return $this->loadFromFile($path);
    }

    /**
     * Removes all auth items of the specified type.
     *
     * @param integer $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
     */
    protected function removeAllItems($type)
    {
        $names = [];
        foreach ($this->items as $name => $item)
        {
            if ($item->type == $type)
            {
                unset($this->items[$name]);
                $names[$name] = true;
            }
        }
        if (empty($names))
        {
            return;
        }

        foreach ($this->assignments as $i => $assignment)
        {
            if (is_array($assignment))
            {
                foreach ($assignment as $eachAssignment)
                {
                    if (isset($names[$eachAssignment->roleName]))
                    {
                        unset($this->assignments[$i]);
                    }
                }
            }
            else
            {
                if (isset($names[$assignment->roleName]))
                {
                    unset($this->assignments[$i]);
                }
            }

        }
        foreach ($this->children as $name => $children)
        {
            if (isset($names[$name]))
            {
                unset($this->children[$name]);
            }
            else
            {
                foreach ($children as $childName => $item)
                {
                    if (isset($names[$childName]))
                    {
                        unset($children[$childName]);
                    }
                }
                $this->children[$name] = $children;
            }
        }

        $this->saveItems();
    }

    protected function load()
    {
        $this->children = [];
        $this->rules = [];
        $this->assignments = [];
        $this->items = [];

        /* 开始加载权限和角色配置 */
        $items = $this->loadFromFile($this->itemFile);
        $itemsModifiedTime = @filemtime($this->itemFile);
        foreach ($items as $name => $item)
        {
            $class = $item['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();

            $this->items[$name] = new $class([
                'name'        => $name,
                'description' => isset($item['description']) ? $item['description'] : null,
                'ruleName'    => isset($item['ruleName']) ? $item['ruleName'] : null,
                'data'        => isset($item['data']) ? $item['data'] : null,
                'createdAt'   => $itemsModifiedTime,
                'updatedAt'   => $itemsModifiedTime,
            ]);
        }
        // 子权限配置
        foreach ($items as $name => $item)
        {
            if (isset($item['children']))
            {
                foreach ($item['children'] as $childName)
                {
                    if (isset($this->items[$childName]))
                    {
                        $this->children[$name][$childName] = $this->items[$childName];
                    }
                    else if (isset($this->originItems[$childName]))
                    {
                        $this->children[$name][$childName] = $this->originItems[$childName];
                    }
                }
            }
        }
        /* 结束加载权限和角色配置 */

        /* 开始检查和处理权限分配 */
        $assignments = $this->loadFromFile($this->assignmentFile);
        $assignmentsModifyTime = @filemtime($this->assignmentFile);
        foreach ($assignments as $userId => $roles)
        {
            foreach ($roles as $role)
            {
                $this->assignments[$userId][$role] = new Assignment([
                    'userId'    => $userId,
                    'roleName'  => $role,
                    'createdAt' => $assignmentsModifyTime,
                ]);
            }
        }
        /* 结束检查和处理权限分配 */

        /* 开始检查和处理规则 */
        $rules = $this->loadFromFile($this->ruleFile);
        $rulesModifyTime = @filemtime($this->ruleFile);
        foreach ($rules as $name => $ruleData)
        {
            if (is_string($ruleData))
            {
                $this->rules[$name] = unserialize($ruleData);
            }
            else
            {
                $ruleData = ArrayHelper::merge($ruleData, [
                    'createdAt' => $rulesModifyTime,
                    'updatedAt' => $rulesModifyTime,
                ]);
                $this->rules[$name] = Yii::createObject($ruleData);
            }
        }
        /* 结束检查和处理规则 */

        if ( ! $this->originItems)
        {
            $this->originItems = $this->items;
        }
    }

    /**
     * @inheritdoc
     */
    public function addChild($parent, $child)
    {
        if ( ! isset($this->originItems[$parent->name], $this->originItems[$child->name]))
        {
            throw new InvalidParamException("Either '{$parent->name}' or '{$child->name}' does not exist.");
        }

        if ($parent->name == $child->name)
        {
            throw new InvalidParamException("Cannot add '{$parent->name} ' as a child of itself.");
        }
        if ($parent instanceof Permission && $child instanceof Role)
        {
            throw new InvalidParamException("Cannot add a role as a child of a permission.");
        }

        if ($this->detectLoop($parent, $child))
        {
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
        }
        if (isset($this->children[$parent->name][$child->name]))
        {
            throw new InvalidCallException("The item '{$parent->name}' already has a child '{$child->name}'.");
        }
        $this->children[$parent->name][$child->name] = $this->originItems[$child->name];
        $this->saveItems();

        return true;
    }

    /**
     * @inheritdoc
     */
    protected function addItem($item)
    {
        $time = time();
        if ($item->createdAt === null)
        {
            $item->createdAt = $time;
        }
        if ($item->updatedAt === null)
        {
            $item->updatedAt = $time;
        }

        $this->items[$item->name] = $item;
        $this->originItems[$item->name] = $item;

        $this->saveItems();

        return true;
    }

    /**
     * 保存认证信息到文件
     *
     * @param array  $data the authorization data
     * @param string $file the file path.
     * @see loadFromFile()
     */
    protected function saveToFile($data, $file)
    {
        if ( ! is_dir(dirname($file)))
        {
            mkdir(dirname($file), 0775, true);
        }
        file_put_contents($file, "<?php" . "\n" . "return " . VarDumper::export($data) . ";\n", LOCK_EX);
    }

    /**
     * @inheritdoc
     */
    public function checkAccess($userId, $permissionName, $params = [])
    {
        $assignments = $this->getAssignments($userId);
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
    }

    /**
     * @inheritdoc
     */
    protected function executeRule($user, $item, $params)
    {
        if ($item->ruleName === null)
        {
            return true;
        }

        $rules = $this->getRule($item->ruleName);
        if ( ! is_array($rules))
        {
            $rules = [$rules];
        }

        $result = true;
        foreach ($rules as $rule)
        {
            if ($result)
            {
                if ($rule instanceof Rule)
                {
                    $return = $rule->execute($user, $item, $params);
                    Yii::info('Check ' . get_class($rule) . ', and result is:' . ($return ? 'true' : 'false'), __METHOD__);
                    $result = $result && $return;
                }
                else
                {
                    throw new InvalidConfigException("Rule not found: {$item->ruleName}");
                }
            }
        }

        return $result;
    }
}
